package com.security.config.security.authentication.common;

import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import com.security.config.security.authentication.username.UsernameAuthenticationToken;
import com.security.constant.RedisConstant;
import com.security.domain.bo.R;
import com.security.domain.vo.AccountInfo;
import com.security.utils.ResponseUtil;
import com.security.utils.ThreadLocalUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Collectors;

/**
 * JWT 过滤器
 *
 * @author DblSun
 */
@SuppressWarnings("all")
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 登录或获取验证码接口放行，不需要验证 Token，其他接口需要验证 Token。
     * 验证 Token 成功时，将用户信息存入 ThreadLocal 中，方便后续使用。
     * 验证 Token 失败时，继续向过滤器链中传递请求，由全局异常处理器处理。
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        // 登录 或 获取验证码 时不需要校验 token
        if (requestURI.equals("/username/login") || requestURI.equals("/phone/login")
                || requestURI.equals("/captcha")) {
            filterChain.doFilter(request, response);
        } else {
            String token = getToken(request.getHeader("Authorization"));
            // 如果 token 为空，则返回给前端
            if (token == null) {
                ResponseUtil.sendResponse(response, R.error(HttpServletResponse.SC_UNAUTHORIZED, "Token 为空或格式不正确"));
                ThreadLocalUtil.remove();
                // 如果不直接 return ,继续向过滤器链中传递请求，会被全局异常处理器当作 RuntimeException NoResourceFoundException 处理
                return;
            }
            boolean verify = false;
            // 如果 token 验证失败，抛异常，返回给前端
            try {
                verify = JWTUtil.verify(token, RedisConstant.TOKEN_SECRET.getBytes());
            } catch (AuthenticationCredentialsNotFoundException e) {
                // 因为使用了第三方工具包，所有验证异常时会抛出 JSONException 异常
                // 全局异常处理器会捕获成 AuthenticationCredentialsNotFoundException 异常
                ResponseUtil.sendResponse(response, R.error(HttpServletResponse.SC_UNAUTHORIZED, "权限认证失败"));
                ThreadLocalUtil.remove();
                filterChain.doFilter(request, response);
            }
            // 将前端传过来的 token 与 redis 中的 token 进行验证
            if (verify) {
                JWTPayload payload = JWTUtil.parseToken(token).getPayload(); // 解析 token，获取 payload
                JSONObject jsonObject = payload.getClaimsJson();// 转换成 JSONObject 对象
                Integer id = jsonObject.getInt("id");
                boolean hasKey = stringRedisTemplate
                        .opsForHash().hasKey(RedisConstant.ApplicationName + RedisConstant.USER_TOKEN, String.valueOf(id));
                // 如果 redis 中没有 token，抛异常，返回给前端
                if (hasKey) {
                    String redisToken = (String) stringRedisTemplate.opsForHash()
                            .get(RedisConstant.ApplicationName + RedisConstant.USER_TOKEN, String.valueOf(id));
                    // 如果 redis 中的 token 和请求头中的 token 不一致，抛异常，返回给前端
                    if (StringUtils.hasText(redisToken) && redisToken.equals(token)) {

                        // 重新包装 AccountInfo 对象
                        String username = jsonObject.getStr("username");
                        String phone = jsonObject.getStr("phone");
                        String email = jsonObject.getStr("email");
                        ArrayList roles = jsonObject.get("roles", ArrayList.class); // roles = ["ROLE_CEO", "ROLE_CFO"]
                        AccountInfo accountInfo = AccountInfo.builder()
                                .id(id).username(username).phone(phone).email(email).roles(roles).build();
                        // 这里不能直接讲 roles.toString() 作为参数，因为传入后会变为 [["ROLE_CEO", "ROLE_CFO"]]
                        // 所以需要将 roles 以 stream 的方式转换成字符串，然后再转换成 Collection<GrantedAuthority>
                        // commaSeparatedStringToAuthorityList 以逗号分隔的字符串转换成 GrantedAuthority 集合 ["ROLE_CEO", "ROLE_CFO"]
                        Collection<GrantedAuthority> authorities =
                                AuthorityUtils.commaSeparatedStringToAuthorityList(
                                        roles.stream().map(String::valueOf).collect(Collectors.joining(",")).toString()
                                );

                        // 存入 ThreadLocal 中，后续其他接口中不用通过 Authentication 获取用户信息，直接在 ThreadLocal 中获取
                        ThreadLocalUtil.set(accountInfo);

                        // 验证通过,重新包装 AuthenticationToken 对象存入 SecurityContext 上下文中
                        UsernameAuthenticationToken usernameAuthenticationToken =
                                new UsernameAuthenticationToken(accountInfo, null, authorities);
                        usernameAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(usernameAuthenticationToken);
                        filterChain.doFilter(request, response);
                    } else {
                        ResponseUtil.sendResponse(response, R.error(HttpServletResponse.SC_UNAUTHORIZED, "Token 已过期"));
                        ThreadLocalUtil.remove();
                        filterChain.doFilter(request, response);
                    }
                } else {
                    ResponseUtil.sendResponse(response, R.error(HttpServletResponse.SC_UNAUTHORIZED, "Token 已过期"));
                    ThreadLocalUtil.remove();
                    filterChain.doFilter(request, response);
                }
            }
        }
    }

    /**
     * 转换请求头中的 token
     */
    public String getToken(String headerToken) {
        if (!StringUtils.hasText(headerToken) || !headerToken.startsWith("Bearer ")) {
            return null;
        }
        return headerToken.substring(7);
    }
}
